using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Charisma.Linq;
using Charisma.MongoDB.Linq;
using Charisma.MongoDb.Linq.Expressions;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Options;
using MongoDB.Driver;

namespace Charisma.MongoDb.Linq
{
    internal class MaterializationHelper<TResult>
    {
// ReSharper disable StaticFieldInGenericType
        private static readonly MethodInfo makeResultReaderMethod = typeof (MaterializationHelper<TResult>).GetMethod("MakeResultReader");
// ReSharper restore StaticFieldInGenericType

        public static Func<MongoCollection, TResult> BuildMaterializer(ProjectionExpression projection, TranslatorContext context)
        {
            Contract.Requires(projection != null);
            Contract.Ensures(Contract.Result<Func<MongoCollection, TResult>>() != null);

            var queryExpression = projection.Query;
            var collectionExpression = (CollectionExpression) queryExpression.Source;
            var collectionElementType = collectionExpression.ElementType;

            
            QueryDocument query;
            try
            {
                var filter = FilterDocumentBuilder.BuildDocument(queryExpression.Filter);
                query = new QueryDocument((IDictionary<string, object>)filter);
            }
            catch
            {
                var filter = JavascriptBuilder.BuildJavascript(queryExpression.Filter);
                query = new QueryDocument { { "$where", filter } };
            }
            var resultElementType = projection.Projector.Type;
            var itemMaterializationLambda = Expression.Lambda(projection.Projector, collectionExpression.Element);
            var collection = Expression.Parameter(typeof (MongoCollection), "c");
            Expression body = Expression.Call(
                LinqHelper.EnumerbleSelectMethod.MakeGenericMethod(collectionElementType, resultElementType),
                Expression.Call(
                    makeResultReaderMethod.MakeGenericMethod(collectionElementType),
                    collection,
                    Expression.Constant(query,  typeof(IMongoQuery)),
                    Expression.Constant(null,   typeof(IMongoSortBy)),
                    Expression.Constant(null,   typeof(IMongoFields)),
                    Expression.Constant(queryExpression.Skip, typeof(int?)),
                    Expression.Constant(queryExpression.Take, typeof(int?))),
                itemMaterializationLambda);

            if (projection.ResultType != ResultType.All)
            {
                var scalarMethodName = projection.ResultType.ToString();
                var enumerableMethod = typeof (Enumerable)
                    .GetMethods(BindingFlags.Static | BindingFlags.Public)
                    .First(m => m.Name == scalarMethodName && m.GetParameters().Length == 1)
                    .MakeGenericMethod(resultElementType);
                body = Expression.Call(enumerableMethod, body);
            }
            else
                body = body.Type == typeof (TResult)
                    ? body
                    : Expression.Convert(body, typeof (TResult));

            var materializerExpression =
                Expression.Lambda<Func<MongoCollection, TResult>>(body, collection);
            var result = materializerExpression.Compile();
            return result;
        }

        public static IEnumerable<TElement> MakeResultReader<TElement>(
            MongoCollection collection, 
            IMongoQuery query, 
            IMongoSortBy sortBy,
            IMongoFields fields,
            int? skip,
            int? take)
        {
            var cursor = collection.FindAs<TElement>(query);
            if (sortBy != null)
                cursor.SetSortOrder(sortBy);
            if (fields != null)
                cursor.SetFields(fields);
            if (skip.HasValue)
                cursor.SetSkip(skip.Value);
            if (take.HasValue)
                cursor.SetLimit(take.Value);
            return cursor;
        }
    }
}